#pragma once

#include <systemd/sd-bus.h>

#include <nlohmann/json_fwd.hpp>
#include <sdbusplus/sdbus.hpp>
#include <sdbusplus/utility/consteval_string.hpp>

#include <exception>
#include <source_location>
#include <string>

namespace sdbusplus
{

enum class UnpackErrorReason
{
    missingProperty,
    wrongType
};

namespace exception
{

/** Base exception class for all sdbusplus exceptions, including those created
 *  by the bindings. */
struct exception : public std::exception
{
    virtual const char* name() const noexcept = 0;
    virtual const char* description() const noexcept = 0;
    virtual int get_errno() const noexcept = 0;

    virtual int set_error(sd_bus_error*) const;
    virtual int set_error(SdBusInterface*, sd_bus_error*) const;

  private:
    // This unused function is to ensure that the vtable for this class is
    // properly emitted when `-flto=auto` is used, which is the default in
    // Yocto builds.  Without this, the vtable is a hidden symbol and no
    // users can inherit from our exception type directly.
    virtual void unused() const noexcept;
};

/** base exception class for all errors created by the sdbus++ generator */
struct generated_exception : public exception
{
    int get_errno() const noexcept override;
};

/** Non-templated base for all new errors and events created by the sdbus++
 * generator */
struct generated_event_base : public generated_exception
{
    virtual auto to_json() const -> nlohmann::json = 0;
    virtual int severity() const noexcept = 0;
};

/** base exception for all new errors and events created by the sdbus++
 * generator */
template <typename Event>
struct generated_event : public generated_event_base
{
    const char* name() const noexcept override
    {
        return Event::errName;
    }

    const char* description() const noexcept override
    {
        return Event::errDesc;
    }

    const char* what() const noexcept override
    {
        return Event::errWhat;
    }

    int get_errno() const noexcept override
    {
        return Event::errErrno;
    }

    int severity() const noexcept override
    {
        return Event::errSeverity;
    }

    template <utility::details::consteval_string_holder V>
    using metadata_t = utility::consteval_string<V>;

  protected:
    mutable std::string jsonString;
};

/** base exception class for all errors generated by sdbusplus itself. */
struct internal_exception : public exception
{};

/** Exception for when an underlying sd_bus method call fails. */
class SdBusError final : public internal_exception
{
  public:
    /** Errno must be positive */
    SdBusError(int error, const char* prefix,
               SdBusInterface* intf = &sdbus_impl);
    SdBusError(int error, std::string&& prefix,
               SdBusInterface* intf = &sdbus_impl);
    /** Becomes the owner of the error */
    SdBusError(sd_bus_error* error, const char* prefix,
               SdBusInterface* intf = &sdbus_impl);

    SdBusError(const SdBusError&) = delete;
    SdBusError& operator=(const SdBusError&) = delete;
    SdBusError(SdBusError&& other);
    SdBusError& operator=(SdBusError&& other);
    ~SdBusError() override;

    const char* name() const noexcept override;
    const char* description() const noexcept override;
    const char* what() const noexcept override;
    int get_errno() const noexcept override;
    const sd_bus_error* get_error() const noexcept;

  private:
    sd_bus_error error;
    std::string full_message;
    SdBusInterface* intf;

    /** Populates the full_message from the stored
     *  error and the passed in prefix. */
    void populateMessage(std::string&& prefix);

    /** Helper to reduce duplicate move logic */
    void move(SdBusError&& other);
};

/** Exception for when an invalid conversion from string to enum is
 *  attempted. */
struct InvalidEnumString final : public internal_exception
{
    static constexpr auto errName =
        "xyz.openbmc_project.sdbusplus.Error.InvalidEnumString";
    static constexpr auto errDesc =
        "An enumeration mapping was attempted for which no valid enumeration "
        "value exists.";
    static constexpr auto errWhat =
        "xyz.openbmc_project.sdbusplus.Error.InvalidEnumString: "
        "An enumeration mapping was attempted for which no valid enumeration "
        "value exists.";

    const char* name() const noexcept override;
    const char* description() const noexcept override;
    const char* what() const noexcept override;
    int get_errno() const noexcept override;
};

/** Exception for when unpackProperties cannot find given property in provided
 * container */
class UnpackPropertyError final : public internal_exception
{
  public:
    UnpackPropertyError(std::string_view propertyName,
                        const UnpackErrorReason reason);

    static constexpr auto errName =
        "xyz.openbmc_project.sdbusplus.Error.UnpackPropertyError";
    static constexpr auto errDesc =
        "unpackProperties failed to unpack one of requested properties.";
    static constexpr auto errWhat =
        "xyz.openbmc_project.sdbusplus.Error.UnpackPropertyError: "
        "unpackProperties failed to unpack one of requested properties.";

    const char* name() const noexcept override;
    const char* description() const noexcept override;
    const char* what() const noexcept override;
    int get_errno() const noexcept override;

    const std::string propertyName;
    const UnpackErrorReason reason;

  private:
    const std::string errWhatDetailed;
};

class UnhandledStop final : public internal_exception
{
  public:
    static constexpr auto errName =
        "xyz.openbmc_project.sdbusplus.Error.UnhandledStop";
    static constexpr auto errDesc =
        "An async Sender failed to handle a stop condition.";
    static constexpr auto errWhat =
        "xyz.openbmc_project.sdbusplus.Error.UnhandledStop: "
        "An async Sender failed to handle a stop condition.";

    const char* name() const noexcept override;
    const char* description() const noexcept override;
    const char* what() const noexcept override;
    int get_errno() const noexcept override;
};

/** Throw a generated_event from the JSON representation.
 *
 *  @param[in] j - JSON representation of the event.
 *  @param[in] source - The source code location of the origin.
 */
void throw_via_json(
    const nlohmann::json& j,
    const std::source_location& source = std::source_location::current());

/** Get the list of known events by name. */
auto known_events() -> std::vector<std::string>;

} // namespace exception

using exception_t = exception::exception;
using internal_exception_t = exception::internal_exception;

} // namespace sdbusplus
